/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.eclipse.ui.editors;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;

/**
 * 
 * @author Brian Remedios
 */
public class StyleExtractor {

    private SyntaxData syntaxData;
    private List<int[]> commentOffsets;

    private static final Color COMMENT_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_DARK_GREEN);
    private static final Color REFERENCED_VAR_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_DARK_GREEN);
    private static final Color COMMENT_BACKGROUND = Display.getDefault().getSystemColor(SWT.COLOR_WHITE);
    private static final Color PUNCTUATION_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
    private static final Color KEYWORD_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_DARK_MAGENTA);
    private static final Color STRING_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_BLUE);

    public StyleExtractor(SyntaxData theSyntaxData) {
        syntaxData = theSyntaxData;
        commentOffsets = new LinkedList<>();
    }

    public void syntax(SyntaxData theSyntax) {
        syntaxData = theSyntax;
    }

    /**
     * Refreshes the offsets for all multiline comments in the parent
     * StyledText. The parent StyledText should call this whenever its text is
     * modified. Note that this code doesn't ignore comment markers inside
     * strings.
     * 
     * @param text
     *            the text from the StyledText
     */
    public void refreshMultilineComments(String text) {
        // Clear any stored offsets
        commentOffsets.clear();

        if (syntaxData != null) {
            // Go through all the instances of COMMENT_START
            int pos = text.indexOf(syntaxData.getMultiLineCommentStart());
            while (pos > -1) {
                // offsets[0] holds the COMMENT_START offset
                // and COMMENT_END holds the ending offset
                int[] offsets = new int[2];
                offsets[0] = pos;
                
                // Find the corresponding end comment.
                pos = text.indexOf(syntaxData.getMultiLineCommentEnd(), pos);
                
                // If no corresponding end comment, use the end of the text
                offsets[1] = pos == -1 ? text.length() - 1 : pos + syntaxData.getMultiLineCommentEnd().length() - 1;
                pos = offsets[1];
                // Add the offsets to the collection
                commentOffsets.add(offsets);

                // next comment
                pos = text.indexOf(syntaxData.getMultiLineCommentStart(), pos);
            }
        }
    }

    /**
     * Checks to see if the specified section of text begins inside a multiline
     * comment. Returns the index of the closing comment, or the end of the line
     * if the whole line is inside the comment. Returns -1 if the line doesn't
     * begin inside a comment.
     * 
     * @param start
     *            the starting offset of the text
     * @param length
     *            the length of the text
     * @return int
     */
    private int getBeginsInsideComment(int start, int length) {
        // Assume section doesn't being inside a comment
        int index = -1;

        // Go through the multiline comment ranges
        for (int i = 0, n = commentOffsets.size(); i < n; i++) {
            int[] offsets = commentOffsets.get(i);

            // If starting offset is past range, quit
            if (offsets[0] > start + length) {
                break;
            }
            // Check to see if section begins inside a comment
            if (offsets[0] <= start && offsets[1] >= start) {
                // It does; determine if the closing comment marker is inside
                // this section
                index = offsets[1] > start + length ? start + length
                        : offsets[1] + syntaxData.getMultiLineCommentEnd().length() - 1;
            }
        }
        return index;
    }

    private boolean isDefinedVariable(String text) {
        return StringUtils.isNotBlank(text);
    }

    private boolean atMultiLineCommentStart(String text, int position) {
        return text.indexOf(syntaxData.getMultiLineCommentStart(), position) == position;
    }

    private boolean atStringStart(String text, int position) {
        return text.indexOf(syntaxData.stringStart, position) == position;
    }

    private boolean atVarnameReference(String text, int position) {
        if (syntaxData.varnameReference == null) {
            return false;
        }
        return text.indexOf(syntaxData.varnameReference, position) == position;
    }

    private boolean atSingleLineComment(String text, int position) {
        if (syntaxData.getComment() == null) {
            return false;
        }
        return text.indexOf(syntaxData.getComment(), position) == position;
    }

    private int getKeywordEnd(String lineText, int start) {

        int length = lineText.length();

        StringBuilder buf = new StringBuilder(length);
        int i = start;

        // Call any consecutive letters a word
        for (; i < length && Character.isLetter(lineText.charAt(i)); i++) {
            buf.append(lineText.charAt(i));
        }

        return syntaxData.isKeyword(buf.toString()) ? i : 0 - i;
    }

    /**
     * Chop up the text into individual lines starting from offset and then
     * determine the required styles for each. Ensures the offset is properly
     * accounted for in each.
     * 
     * @param text
     * @param offset
     * @param length
     * @return
     */
    public List<StyleRange> stylesFor(String text, int offset, int length, String lineSeparator) {

        if (syntaxData == null) {
            return Collections.emptyList();
        }

        String content = text.substring(offset, offset + length);
        String[] lines = content.split(lineSeparator);

        List<StyleRange> styles = new ArrayList<>();

        int separatorLength = lineSeparator.length();

        int currentOffset = offset;

        for (String line : lines) {
            int lineLength = line.length();
            List<StyleRange> lineStyles = lineStylesFor(line, 0, lineLength);

            for (StyleRange sr : lineStyles) {
                sr.start += currentOffset;
            }
            styles.addAll(lineStyles);
            currentOffset += lineLength + separatorLength;
        }

        return styles;
    }

    public List<StyleRange> lineStylesFor(String lineText, int lineOffset, int length) {

        List<StyleRange> styles = new ArrayList<>();

        int start = 0;

        // Check if line begins inside a multiline comment
        int mlIndex = getBeginsInsideComment(lineOffset, lineText.length());
        if (mlIndex > -1) {
            // Line begins inside multiline comment; create the range
            styles.add(new StyleRange(lineOffset, mlIndex - lineOffset, COMMENT_COLOR, COMMENT_BACKGROUND));
            start = mlIndex;
        }
        // Do punctuation, single-line comments, and keywords
        while (start < length) {
            // Check for multiline comments that begin inside this line
            if (atMultiLineCommentStart(lineText, start)) {
                // Determine where comment ends
                int endComment = lineText.indexOf(syntaxData.getMultiLineCommentEnd(), start);

                // If comment doesn't end on this line, extend range to end of
                // line
                if (endComment == -1) {
                    endComment = length;
                } else {
                    endComment += syntaxData.getMultiLineCommentEnd().length();
                }
                styles.add(new StyleRange(lineOffset + start, endComment - start, COMMENT_COLOR, COMMENT_BACKGROUND));

                start = endComment;
            } else if (atStringStart(lineText, start)) {
                // Determine where comment ends
                int endString = lineText.indexOf(syntaxData.stringEnd, start + 1);

                // If string doesn't end on this line, extend range to end of
                // line
                if (endString == -1) {
                    endString = length;
                } else {
                    endString += syntaxData.stringEnd.length();
                }
                styles.add(new StyleRange(lineOffset + start, endString - start, STRING_COLOR, COMMENT_BACKGROUND));

                start = endString;
            } else if (atSingleLineComment(lineText, start)) {
                // Check for single line comments

                styles.add(new StyleRange(lineOffset + start, length - start, COMMENT_COLOR, COMMENT_BACKGROUND));
                start = length;
            } else if (atVarnameReference(lineText, start)) {
                // Check for variable references

                StringBuilder buf = new StringBuilder();
                int i = start + syntaxData.getVarnameReference().length();
                // Call any consecutive letters a word
                for (; i < length && Character.isLetter(lineText.charAt(i)); i++) {
                    buf.append(lineText.charAt(i));
                }

                // See if the word is a variable
                if (isDefinedVariable(buf.toString())) {
                    // It's a keyword; create the StyleRange
                    styles.add(new StyleRange(lineOffset + start, i - start, REFERENCED_VAR_COLOR, null, SWT.BOLD));
                }
                // Move the marker to the last char (the one that wasn't a
                // letter)
                // so it can be retested in the next iteration through the loop
                start = i;
            } else if (syntaxData.isPunctuation(lineText.charAt(start))) {
                // Check for punctuation
                // Add range for punctuation
                styles.add(new StyleRange(lineOffset + start, 1, PUNCTUATION_COLOR, null));
                ++start;
            } else if (Character.isLetter(lineText.charAt(start))) {
                // See if the word is a keyword
                int kwEnd = getKeywordEnd(lineText, start); 

                if (kwEnd > start) { // Its a keyword; create the StyleRange
                    styles.add(new StyleRange(lineOffset + start, kwEnd - start, KEYWORD_COLOR, null));
                }

                // Move the marker to the last char (the one that wasn't a
                // letter)
                // so it can be retested in the next iteration through the loop
                start = Math.abs(kwEnd);
            } else {
                ++start; // It's nothing we're interested in; advance the marker
            }
        }

        return styles;
    }
}
